using System;

namespace Legend.Caching
{
    /// <summary>
    /// Contains static methods, extensions and helpers for the ICache{TKey, TValue} interface.
    /// </summary>
    public static class Cache
    {
        /// <summary>
        /// Gets the value with the specified key, if the value does not exist
        /// the valueProducer delegate is called and the produced value is inserted into
        /// the dictionary and then returned.
        /// </summary>
        /// <param name="key">The key to get the value for.</param>
        /// <param name="valueProducer">Called if the key does not exist in the cache.</param>
        /// <param name="cache">The cache to get the value from.</param>
        /// <param name="timeToLive">The time to live if the value produced by the value producer is inserted in the cache.</param>
        /// <returns>The value stored in the dictionary by the specified key or the value from the valueProducer delegate.</returns>
        public static T GetValue<T>(this ICache cache, string key, TimeSpan timeToLive, Func<T> valueProducer)
        {
            Require.ThatArgument(cache).Named("cache").IsNotNull();
            Require.ThatArgument(key).Named("key").IsNotNull();
            Require.ThatArgument(valueProducer).Named("valueProducer").IsNotNull();

            object result = null;

            if (!cache.TryGetValue(key, out result))
            {
                result = valueProducer();
                cache.Insert(key, result, timeToLive);
            }

            return (T)result;
        }

        /// <summary>
        /// Tries to get a value from the cache.
        /// </summary>
        /// <typeparam name="T">The type of the value to get.</typeparam>
        /// <param name="cache">The cache to get the value from.</param>
        /// <param name="key">The key of the value to get.</param>
        /// <param name="value">Output variable for the value.</param>
        /// <returns>True if the value was found in the cache.</returns>
        public static bool TryGetValue<T>(this ICache cache, string key, out T value)
        {
            object retrievedValue = null;
            
            if (cache.TryGetValue(key, out retrievedValue) )
            {
                if (ValueIsAssignableToType(typeof(T), retrievedValue))
                {
                    value = (T)retrievedValue;
                    return true;
                }
            }

            value = default(T);
            return false;
        }

        public static T GetValueOrDefault<T>(this ICache cache, string key)
        {
            Require.ThatArgument(cache).Named("cache").IsNotNull();

            T result;
            cache.TryGetValue(key, out result);
            return result;
        }

        private static bool ValueIsAssignableToType(Type type, object retrievedValue)
        {
            return 
                type.IsClass
                || (retrievedValue != null && type.IsAssignableFrom(retrievedValue.GetType()))
                || (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>)));
        }
    }
}
